﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Linq;
using OSGeo.MapGuide;
using System.Data;
using System.Xml;
using System.Xml.Serialization;
using OSGeo.MapGuide.ObjectModels;

namespace OSGeo.MapGuide
{
    public static class ExtensionMethods
    {
        static MgAgfReaderWriter agfIO;
        static MgWktReaderWriter wktIO;

        static ExtensionMethods()
        {
            agfIO = new MgAgfReaderWriter();
            wktIO = new MgWktReaderWriter();
        }

        /// <summary>
        /// Converts a CLR DateTime to a MapGuide DateTime
        /// </summary>
        /// <param name="dt"></param>
        /// <returns></returns>
        public static MgDateTime AsMgDateTime(this DateTime dt)
        {
            return new MgDateTime((short)dt.Year, (short)dt.Month, (short)dt.Day, (short)dt.Hour, (short)dt.Minute, (short)dt.Second, (short)(dt.Millisecond / 1000));
        }

        /// <summary>
        /// Converts a MapGuide DateTime to a CLR DateTime
        /// </summary>
        /// <param name="dt"></param>
        /// <returns></returns>
        public static DateTime AsDateTime(this MgDateTime dt)
        {
            if (null == dt)
                throw new ArgumentNullException("dt");

            return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Microsecond * 1000);
        }

        /// <summary>
        /// Converts a byte reader to a byte array
        /// </summary>
        /// <param name="breader"></param>
        /// <returns></returns>
        public static byte[] AsByteArray(this MgByteReader breader)
        {
            if (null == breader)
                throw new ArgumentNullException("breader");

            byte[] data = new byte[breader.GetLength()];
            breader.Read(data, data.Length);
            breader.Rewind();
            return data;
        }

        /// <summary>
        /// Converts a byte array to a byte reader
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public static MgByteReader AsByteReader(this byte[] bytes)
        {
            if (null == bytes)
                throw new ArgumentNullException("bytes");

            MgByteSource source = new MgByteSource(bytes, bytes.Length);
            return source.GetReader();
        }

        /// <summary>
        /// Converts a byte reader to a MapGuide Geometry
        /// </summary>
        /// <param name="agf"></param>
        /// <returns></returns>
        public static MgGeometry AsGeometry(this MgByteReader agf)
        {
            if (null == agf)
                throw new ArgumentNullException("agf");

            //if (agf.MimeType != MgMimeType.Agf)
            //    throw new ArgumentException("Byte reader is of incorrect mime type", "agf");

            return agfIO.Read(agf);
        }

        /// <summary>
        /// Converts a MapGuide Geometry to WKT text
        /// </summary>
        /// <param name="geom"></param>
        /// <returns></returns>
        public static string AsWkt(this MgGeometry geom)
        {
            if (null == geom)
                throw new ArgumentNullException("geom");

            return wktIO.Write(geom);
        }

        /// <summary>
        /// Attempts to get a BLOB property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgBlobProperty TryGetBlobProperty(this MgFeatureReader reader, string name, MgByteReader defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgBlobProperty(name, defaultValue);
            else
                return new MgBlobProperty(name, reader.GetBLOB(name));
        }

        /// <summary>
        /// Attempts to get a boolean property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgBooleanProperty TryGetBooleanProperty(this MgFeatureReader reader, string name, bool defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgBooleanProperty(name, defaultValue);
            else
                return new MgBooleanProperty(name, reader.GetBoolean(name));
        }

        /// <summary>
        /// Attempts to get a byte property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgByteProperty TryGetByteProperty(this MgFeatureReader reader, string name, byte defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgByteProperty(name, defaultValue);
            else
                return new MgByteProperty(name, reader.GetByte(name));
        }

        /// <summary>
        /// Attempts to get a CLOB property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgClobProperty TryGetClobProperty(this MgFeatureReader reader, string name, MgByteReader defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgClobProperty(name, defaultValue);
            else
                return new MgClobProperty(name, reader.GetCLOB(name));
        }

        /// <summary>
        /// Attempts to get a DateTime property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgDateTimeProperty TryGetDateTimeProperty(this MgFeatureReader reader, string name, MgDateTime defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgDateTimeProperty(name, defaultValue);
            else
                return new MgDateTimeProperty(name, reader.GetDateTime(name));
        }

        /// <summary>
        /// Attempts to get a double property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgDoubleProperty TryGetDoubleProperty(this MgFeatureReader reader, string name, double defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgDoubleProperty(name, defaultValue);
            else
                return new MgDoubleProperty(name, reader.GetDouble(name));
        }

        /// <summary>
        /// Attempts to get a feature property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgFeatureProperty TryGetFeatureProperty(this MgFeatureReader reader, string name, MgFeatureReader defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgFeatureProperty(name, defaultValue);
            else
                return new MgFeatureProperty(name, reader.GetFeatureObject(name));
        }

        /// <summary>
        /// Attempts to get an int16 property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgInt16Property TryGetInt16Property(this MgFeatureReader reader, string name, short defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgInt16Property(name, defaultValue);
            else
                return new MgInt16Property(name, reader.GetInt16(name));
        }

        /// <summary>
        /// Attempts to get an int32 property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgInt32Property TryGetInt32Property(this MgFeatureReader reader, string name, int defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgInt32Property(name, defaultValue);
            else
                return new MgInt32Property(name, reader.GetInt32(name));
        }

        /// <summary>
        /// Attempts to get an int64 property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgInt64Property TryGetInt64Property(this MgFeatureReader reader, string name, long defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgInt64Property(name, defaultValue);
            else
                return new MgInt64Property(name, reader.GetInt64(name));
        }

        /// <summary>
        /// Attempts to get a single property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgSingleProperty TryGetSingleProperty(this MgFeatureReader reader, string name, float defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgSingleProperty(name, defaultValue);
            else
                return new MgSingleProperty(name, reader.GetSingle(name));
        }

        /// <summary>
        /// Attempts to get a string property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgStringProperty TryGetStringProperty(this MgFeatureReader reader, string name, string defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgStringProperty(name, defaultValue);
            else
                return new MgStringProperty(name, reader.GetString(name));
        }

        /// <summary>
        /// Attempts to get a geometry property, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgGeometryProperty TryGetGeometryProperty(this MgFeatureReader reader, string name, MgByteReader defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            if (reader.IsNull(name))
                return new MgGeometryProperty(name, defaultValue);
            else
                return new MgGeometryProperty(name, reader.GetGeometry(name));
        }

        /// <summary>
        /// Attempts to get a byte value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static byte? TryGetByte(this MgFeatureReader reader, string name, byte? defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetByte(name);
        }

        /// <summary>
        /// Attempts to get a boolean value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static bool? TryGetBoolean(this MgFeatureReader reader, string name, bool? defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetBoolean(name);
        }

        /// <summary>
        /// Attempts to get a BLOB value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgByteReader TryGetBlob(this MgFeatureReader reader, string name, MgByteReader defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetBLOB(name);
        }

        /// <summary>
        /// Attempts to get a CLOB value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgByteReader TryGetClob(this MgFeatureReader reader, string name, MgByteReader defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetCLOB(name);
        }

        /// <summary>
        /// Attempts to get a DateTime value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgDateTime TryGetDateTime(this MgFeatureReader reader, string name, MgDateTime defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetDateTime(name);
        }

        /// <summary>
        /// Attempts to get a double value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static double? TryGetDouble(this MgFeatureReader reader, string name, double? defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetDouble(name);
        }

        /// <summary>
        /// Attempts to get an int16 value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static short? TryGetInt16(this MgFeatureReader reader, string name, short? defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetInt16(name);
        }

        /// <summary>
        /// Attempts to get an int32 value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static int? TryGetInt32(this MgFeatureReader reader, string name, int? defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetInt32(name);
        }

        /// <summary>
        /// Attempts to get an int64 value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static long? TryGetInt64(this MgFeatureReader reader, string name, long? defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetInt64(name);
        }

        /// <summary>
        /// Attempts to get a single value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static float? TryGetSingle(this MgFeatureReader reader, string name, float? defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetSingle(name);
        }

        /// <summary>
        /// Attempts to get a string value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static string TryGetString(this MgFeatureReader reader, string name, string defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetString(name);
        }

        /// <summary>
        /// Attempts to get a geometry value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgByteReader TryGetGeometry(this MgFeatureReader reader, string name, MgByteReader defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetGeometry(name);
        }

        /// <summary>
        /// Attempts to get a feature value, returning a default value if no value could be read
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static MgFeatureReader TryGetFeature(this MgFeatureReader reader, string name, MgFeatureReader defaultValue)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            return reader.IsNull(name) ? defaultValue : reader.GetFeatureObject(name);
        }

        /// <summary>
        /// Gets an array of all the property names in the class definition
        /// </summary>
        /// <param name="cls"></param>
        /// <returns></returns>
        public static string[] GetPropertyNames(this MgClassDefinition cls)
        {
            if (null == cls)
                throw new ArgumentNullException("cls");

            List<string> propNames = new List<string>();
            foreach (MgPropertyDefinition propDef in cls.GetProperties())
            {
                propNames.Add(propDef.Name);
            }
            return propNames.ToArray();
        }

        /// <summary>
        /// Reads a single feature into a property collection. The caller is responsible for advancing the feature
        /// reader.
        /// </summary>
        /// <param name="reader"></param>
        /// <param name="propertyNames"></param>
        /// <returns></returns>
        public static MgPropertyCollection ReadFeature(this MgFeatureReader reader, string[] propertyNames)
        {
            if (null == reader)
                throw new ArgumentNullException("reader");

            if (null == propertyNames)
                throw new ArgumentNullException("propertyNames");

            MgPropertyCollection props = new MgPropertyCollection();

            foreach (string propName in propertyNames)
            {
                MgProperty prop = null;

                switch (reader.GetPropertyType(propName))
                {
                    case MgPropertyType.Blob:
                        prop = reader.TryGetBlobProperty(propName, null);
                        break;
                    case MgPropertyType.Boolean:
                        prop = reader.TryGetBooleanProperty(propName, default(bool));
                        break;
                    case MgPropertyType.Byte:
                        prop = reader.TryGetByteProperty(propName, default(byte));
                        break;
                    case MgPropertyType.Clob:
                        prop = reader.TryGetClobProperty(propName, null);
                        break;
                    case MgPropertyType.DateTime:
                        prop = reader.TryGetDateTimeProperty(propName, null);
                        break;
                    case MgPropertyType.Double:
                        prop = reader.TryGetDoubleProperty(propName, default(double));
                        break;
                    case MgPropertyType.Feature:
                        prop = reader.TryGetFeatureProperty(propName, null);
                        break;
                    case MgPropertyType.Geometry:
                        prop = reader.TryGetGeometryProperty(propName, null);
                        break;
                    case MgPropertyType.Int16:
                        prop = reader.TryGetInt16Property(propName, default(short));
                        break;
                    case MgPropertyType.Int32:
                        prop = reader.TryGetInt32Property(propName, default(int));
                        break;
                    case MgPropertyType.Int64:
                        prop = reader.TryGetInt64Property(propName, default(long));
                        break;
                    case MgPropertyType.Single:
                        prop = reader.TryGetSingleProperty(propName, default(float));
                        break;
                    case MgPropertyType.String:
                        prop = reader.TryGetStringProperty(propName, null);
                        break;
                }

                if (prop != null)
                {
                    props.Add(prop);
                }
            }

            return props;
        }

        /// <summary>
        /// Closes all feature readers in the batch property collection
        /// </summary>
        /// <param name="features"></param>
        public static void ReleaseReaders(this MgBatchPropertyCollection features)
        {
            if (null == features)
                throw new ArgumentNullException("features");

            foreach (MgPropertyCollection feature in features)
            {
                feature.ReleaseReaders();
            }
        }

        /// <summary>
        /// Closes all feature readers in the property collection
        /// </summary>
        /// <param name="props"></param>
        public static void ReleaseReaders(this MgPropertyCollection props)
        {
            if (null == props)
                throw new ArgumentNullException("props");

            foreach (MgProperty p in props)
            {
                MgFeatureProperty fp = p as MgFeatureProperty;
                if (fp != null)
                {
                    MgFeatureReader reader = fp.Value;
                    reader.Close();
                }
            }
        }

        /// <summary>
        /// Determines if a set of insert operations was successful. Success is defined as NO failures whatsoever.
        /// </summary>
        /// <param name="result"></param>
        /// <returns></returns>
        public static bool Success(this MgInsertResult[] result)
        {
            return !result.Any(x => !x.Success);
        }

        /// <summary>
        /// Gets a list of features that failed to insert after a <see cref="InsertBatch"/> operation on <see cref="MgFeatureRepository"/>
        /// </summary>
        /// <param name="result"></param>
        /// <param name="origFeatures"></param>
        /// <returns></returns>
        public static IEnumerable<MgPropertyCollection> GetFailedFeatures(this MgInsertResult[] result, MgBatchPropertyCollection origFeatures)
        {
            if (null == result)
                throw new ArgumentNullException("result");

            if (null == origFeatures)
                throw new ArgumentNullException("origFeatures");

            if (result.Count() != origFeatures.Count())
                throw new ArgumentException("Size of original feature collection does not match the size of the insert results", "origFeatures");

            for (int i = 0; i < result.Length; i++)
            {
                if (!result[i].Success)
                    yield return origFeatures[i];
            }
        }

        /// <summary>
        /// Loads the features into a <see cref="DataTable"/>. Calling this method will close the reader.
        /// </summary>
        /// <param name="reader"></param>
        /// <returns></returns>
        public static DataTable ToDataTable(this MgFeatureReader reader)
        {
            MgClassDefinition cls = reader.GetClassDefinition();
            DataTable table = cls.CreateDataTable();

            while (reader.ReadNext())
            {
                DataRow row = table.NewRow();



                table.Rows.Add(row);
            }

            reader.Close();

            return table;
        }

        /// <summary>
        /// Creates an empty <see cref="DataTable"/> from an <see cref="MgClassDefinition"/> instance
        /// </summary>
        /// <param name="cls"></param>
        /// <returns></returns>
        private static DataTable CreateDataTable(this MgClassDefinition cls)
        {
            DataTable table = new DataTable(cls.Name);
            List<DataColumn> pks = new List<DataColumn>();
            foreach (MgPropertyDefinition pd in cls.GetProperties())
            {
                string pName = pd.Name;
                DataColumn col = null;
                switch (pd.PropertyType)
                {
                    case MgFeaturePropertyType.DataProperty:
                        {
                            MgDataPropertyDefinition dp = (MgDataPropertyDefinition)pd;
                            switch (dp.DataType)
                            {
                                case MgPropertyType.Blob:
                                    col = new DataColumn(pName, typeof(byte[]));
                                    break;
                                case MgPropertyType.Boolean:
                                    col = new DataColumn(pName, typeof(bool));
                                    break;
                                case MgPropertyType.Byte:
                                    col = new DataColumn(pName, typeof(byte));
                                    break;
                                case MgPropertyType.Clob:
                                    throw new NotSupportedException("Clob properties are not supported");
                                case MgPropertyType.DateTime:
                                    col = new DataColumn(pName, typeof(DateTime));
                                    break;
                                case MgPropertyType.Double:
                                    col = new DataColumn(pName, typeof(double));
                                    break;
                                case MgPropertyType.Feature:
                                    throw new NotSupportedException("Feature properties are not supported");
                                case MgPropertyType.Int16:
                                    col = new DataColumn(pName, typeof(short));
                                    break;
                                case MgPropertyType.Int32:
                                    col = new DataColumn(pName, typeof(int));
                                    break;
                                case MgPropertyType.Int64:
                                    col = new DataColumn(pName, typeof(long));
                                    break;
                                case MgPropertyType.Raster:
                                    throw new NotSupportedException("Raster properties are not supported");
                                case MgPropertyType.Single:
                                    col = new DataColumn(pName, typeof(float));
                                    break;
                                case MgPropertyType.String:
                                    col = new DataColumn(pName, typeof(string));
                                    break;
                            }

                            if (col != null)
                            {
                                if (cls.GetIdentityProperties().Contains(pName))
                                    pks.Add(col);
                            }
                        }
                        break;
                    case MgFeaturePropertyType.GeometricProperty:
                        col = new DataColumn(pName, typeof(MgGeometryProxy));
                        break;
                }

                if (col != null)
                    table.Columns.Add(col);
            }
            table.PrimaryKey = pks.ToArray();
            return table;
        }

        /// <summary>
        /// Creates a new ad-hoc extended feature class and selects features from it.
        /// </summary>
        /// <param name="svc"></param>
        /// <param name="joinDef"></param>
        /// <param name="filter"></param>
        /// <param name="resSvc"></param>
        /// <returns></returns>
        public static MgFeatureReader SelectJoinedFeatures(this MgFeatureService svc, MgJoinDefinition joinDef, string filter, MgResourceService resSvc)
        {
            /*
             * What we're basically doing is programatically creating a new ad-hoc feature source that contains our extended
             * feature class specification. 
             * 
             * Once the new feature source is created, we just query from it.
             */
            
            MgByteReader leftFsContent = resSvc.GetResourceContent(joinDef.Left.FeatureSource);
            FeatureSourceType leftFeatureSource = null;
            XmlSerializer ser = new XmlSerializer(typeof(FeatureSourceType));
            using (MemoryStream ms = new MemoryStream())
            {
                byte[] b = new byte[leftFsContent.GetLength()];
                leftFsContent.Read(b, b.Length);
                ms.Write(b, 0, b.Length);
                ms.Position = 0;
                leftFeatureSource = (FeatureSourceType)ser.Deserialize(ms);
            }

            List<RelatePropertyType> relations = new List<RelatePropertyType>();
            foreach (MgStringPair pair in joinDef.JoinProperties)
            {
                relations.Add(new RelatePropertyType()
                {
                    FeatureClassProperty = pair.Key,
                    AttributeClassProperty = pair.Value
                });
            }

            FeatureSourceType joinFeatureSource = new FeatureSourceType()
            {
                Provider = leftFeatureSource.Provider,
                Parameter = leftFeatureSource.Parameter,
                Extension = new FeatureSourceTypeExtension[] 
                {
                    new FeatureSourceTypeExtension()
                    {
                        AttributeRelate = new AttributeRelateType[] {
                            new AttributeRelateType()
                            {
                                AttributeClass = joinDef.Right.ClassName,
                                AttributeNameDelimiter = joinDef.Right.Prefix,
                                ForceOneToOne = (joinDef.Cardinality == MgJoinCardinalityType.OneToMany) && joinDef.ForceOneToOne,
                                Name = "",
                                RelateProperty = relations.ToArray(),
                                RelateType = joinDef.JoinType,
                                ResourceId = joinDef.Right.FeatureSource.ToString()
                            }
                        },
                        FeatureClass = joinDef.Left.ClassName,
                        Name = joinDef.ExtendedClassName 
                    }
                }
            };

            using (MemoryStream ms = new MemoryStream())
            {
                ser.Serialize(ms, joinFeatureSource);
                byte[] buffer = ms.ToArray();
                MgByteSource source = new MgByteSource(buffer, buffer.Length);

                resSvc.SetResource(joinDef.JoinResourceId, source.GetReader(), null);
            }

            MgFeatureQueryOptions query = new MgFeatureQueryOptions();
            if (!string.IsNullOrEmpty(filter))
                query.SetFilter(filter);

            MgFeatureReader reader = svc.SelectFeatures(joinDef.JoinResourceId, joinDef.ExtendedClassName, query);
            return reader;
        }
    }
}
